/**
 *  Stream Recorder: Records RealPlayer(tm) streams as MP3s
 *  Copyright (C) 2007  David Griffiths
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

package dg.radio2ipod.model;

import dg.inx.AbstractModel;
import dg.radio2ipod.Main;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Properties;
import java.util.ResourceBundle;
import dg.radio2ipod.Util;
import java.io.FileInputStream;
import java.util.Date;
import java.util.Vector;
import javazoom.jl.player.Player;

/**
 * A stream that is going to be, or is recorded. These objects are
 * facades for the mplayer and lame processes that do the actual
 * recording work.
 * @author davidg
 */
public final class Stream extends AbstractModel {
    /**
     * Stream is in the queue and has yet to start recording.
     */
    public static final int UNSTARTED = 0;
    /**
     * Stream is buffering enough information to begin recording.
     */
    public static final int BUFFERING = 1;
    /**
     * Stream is being recorded as MP3.
     */
    public static final int READING = 2;
    /**
     * Stream completely recorded successfully.
     */
    public static final int SUCCESSFUL = 4;
    /**
     * Stream failed when trying to record.
     */
    public static final int FAILED = 8;
    /**
     * Per-centage point at which cache is full and file starts to be
     * written.
     */
    static final double CACHE_THRESHOLD = 18.75;
    /**
     * Number of milli-seconds in a second.
     */
    static final long MILLIS_PER_SECOND = 1000L;
    /**
     * Stream of objects listening to the stream process.
     */
    private final Vector listeners = new Vector();

    private static ResourceBundle resBundle = ResourceBundle.getBundle(
            "dg/radio2ipod/resource/strings");

    private static File binDir;

    private int status = UNSTARTED;
    private long startTick = 0;
    private String source = "";
    private String genre = "";
    private String artist = "";
    private String album = "";
    private String title = "";
    private String comments = "";
    private int perCentProgress;
    private boolean importITunes;
    private boolean stereo;
    private Date date;

    static {
        String bd = System.getProperty("resources.dir") + "bin/";
        if (bd == null) {
            System.err.println(resBundle.getString(
                    "bin_directory_not_defined"));
        }
        String strmrip = bd + "strmrip";
        if (!(new File(strmrip)).exists()) {
            System.err.println(resBundle.getString("file") + strmrip
                    + " " + resBundle.getString("does_not_exist"));
        }
        binDir = new File(bd);
    }

    Stream(final String theSource) {
        this.source = theSource;
    }

    /**
     * Save the stream to the given file, and keep the given action listener
     * up to date with changes in status.
     * @param f file to save to
     * @param listener action listener to notify as the status of the recording
     * process changes
     */
    public void save(final File f, final ActionListener listener) {
        try {
            throw new RuntimeException("dummy");
            
        } catch(Exception e) {
            e.printStackTrace();
        }
        (new Thread() {
            public void run() {
                saveImpl(f, listener);
            }
        }).start();
    }

    /**
     * Stop all recording processes. Note: at the moment this method kills
     * off all recording, even if they have been started by other instances of
     * stream recorder.
     */
    public static void stopAllRecording() {
        Process process = null;

        DataInputStream in = null;

        try {
            process = Runtime.getRuntime().exec(new String[] {"sh", "killmp"},
                    null, binDir);

            in = new DataInputStream(process.getInputStream());

            String s = in.readLine();

            while (s != null) {
                s = in.readLine();
            }
            process.waitFor();
            if (process.exitValue() != 0) {
                System.err.println(resBundle.getString("Warning:_kill_failed"));
            }

        } catch (IOException ioe) {
            ioe.printStackTrace();
        } catch (InterruptedException ie) {
            ie.printStackTrace();
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException ioe) {
                    ioe.printStackTrace();
                }
            }
        }
    }

    private void saveImpl(final File f, final ActionListener listener) {
        Properties props = System.getProperties();

        Process process = null;
        DataInputStream in = null;

        try {
            ArrayList c = new ArrayList();
            c.add("sh");
            c.add("strmrip");
            c.add("-I");
            c.add("-o");
            c.add(f.getAbsolutePath());
            if (isStereo()) {
                c.add("-m");
                c.add("s");
            }
            if (getArtist() != null) {
                c.add("-a");
                c.add(getArtist());
            }
            if (getGenre() != null) {
                c.add("-g");
                c.add(this.getGenre());
            }
            if (this.getAlbum() != null) {
                c.add("-l");
                c.add(this.getAlbum());
            }
            if (getTitle() != null) {
                c.add("-t");
                c.add(this.getTitle());
            }
            if (getComments() != null) {
                c.add("-c");
                c.add("\"" + this.getComments() + "\"");
            }
            c.add(this.getSource());
            String[] cmd = new String[c.size()];
            for (int i = 0; i < cmd.length; i++) {
                cmd[i] = (String) c.get(i);
                System.out.println(cmd[i]);
            }
            process = Runtime.getRuntime().exec(cmd, null, binDir);
            setStatus(BUFFERING);
            if (listener != null) {
                listener.actionPerformed(new ActionEvent(this, this.getStatus(),
                        null));
            }

            in = new DataInputStream(process.getInputStream());

            String s = in.readLine();
            notifyListeners(s);

            while (s != null) {
                if (s.indexOf("Exiting") != -1) {
                    this.stopAllRecording();
                    break;
                }
                if (s.startsWith("LAME")) {
                    this.startTick = System.currentTimeMillis();
                    this.status = READING;
                    (new Thread() {
                        public void run() {
                            try {
                                Thread.sleep(5000); // Pause to allow caching
                                FileInputStream in = new FileInputStream(f);
                                Player player = new Player(in);
                                player.play();
                            } catch(Exception e) {
                                e.printStackTrace();
                            }
                        }
                    }).start();
                    if (listener != null) {
                        listener.actionPerformed(new ActionEvent(this,
                                this.getStatus(), null));
                    }
                } else if (s.startsWith("Cache fill:")) {
                    int perCentPos = s.indexOf('%');
                    if (perCentPos != -1) {
                        String cacheFilePC = s.substring("Cache fill:".length(),
                                perCentPos).trim();
                        double pcDouble = (new Double(cacheFilePC)
                        ).doubleValue();
                        int perCent = (int) (100 * pcDouble / CACHE_THRESHOLD);
                        if (perCent > 100) {
                            perCent = 100;
                        }
                        if (perCent != getPerCentProgress()) {
                            this.perCentProgress = perCent;
                            if (listener != null) {
                                listener.actionPerformed(new ActionEvent(this,
                                        this.getStatus(), null));
                            }
                        }
                    }
                }
                s = in.readLine();
                notifyListeners(s);
            }

            process.waitFor();
            Main.getMainFrame().consoleWrite("Recording finished");

            if (process.exitValue() != 0) {
                String error = resBundle.getString(
                        "Warning:_strmrip_failed");
                Util.errorMessage(error);
            }

        } catch (Exception e) {
            Util.errorMessage(e.getMessage());
            e.printStackTrace();
        } finally {
            this.clearListeners();
            if (in != null) {
                try {
                    in.close();
                } catch (IOException ioe) {
                    Util.errorMessage(ioe.getMessage());
                    ioe.printStackTrace();
                }
            }
        }

        if ((process != null) && (process.exitValue() == 0)) {
            this.status = SUCCESSFUL;
            if (listener != null) {
                listener.actionPerformed(new ActionEvent(this,
                        this.getStatus(), null));
            }
        }
        this.status = FAILED;
        if (listener != null) {
            listener.actionPerformed(new ActionEvent(this, this.getStatus(),
                    null));
        }
    }

    /**
     * The number of seconds that the recording have been in progress.
     * @return number of secodns that the recording have been in progress
     */
    public int getSecondsRunning() {
        if (this.status == BUFFERING) {
            return (int) ((System.currentTimeMillis() - this.startTick)
            / MILLIS_PER_SECOND);
        }
        return 0;
    }

    /**
     * Status of the recording. Generally one of
     * {@link #UNSTARTED}, {@link #BUFFERING}, {@link #READING},
     * {@link #SUCCESSFUL}, {@link #FAILED}.
     * @return status of the recording
     */
    public int getStatus() {
        return status;
    }

    private void setStatus(final int theStatus) {
        int oldStatus = this.status;
        this.status = theStatus;
        this.firePropertyChange("status", oldStatus, this.status);
    }

    /**
     * The genre of the recording (eg Blues, Speech).
     * @return genre of this stream
     */
    public String getGenre() {
        return genre;
    }

    /**
     * Specify the theGenre of this stream (eg Blues, Audiobook). This will
     * be written into the meta-data of the MP3. Property listeners are
     * notified if this value changes.
     *
     * @param theGenre theGenre of the stream
     */
    public void setGenre(final String theGenre) {
        String oldGenre = this.genre;
        this.genre = theGenre;
        this.firePropertyChange("status", oldGenre, this.genre);
    }

    /**
     * Artist of this stream.
     * @return artist of this stream
     */
    public String getArtist() {
        return artist;
    }

    /**
     * Specify the theArtist featured on this stream. This will be written into
     * the meta-data of the MP3. Property listeners
     * are notified if this value changes.
     *
     * @param theArtist theArtist specified on this stream
     */
    public void setArtist(final String theArtist) {
        String oldArtist = this.artist;
        this.artist = theArtist;
        this.firePropertyChange("artist", oldArtist, this.artist);
    }

    /**
     * Album that this recording belongs to.
     * @return album this recording belongs to
     */
    public String getAlbum() {
        return album;
    }

    /**
     * Specify the theAlbum this recording belongs to. This will be written into
     * the meta-data of the MP3. Property listeners
     * are notified if this value changes.
     *
     * @param theAlbum theAlbum this recording belongs to
     */
    public void setAlbum(final String theAlbum) {
        String oldAlbum = this.album;
        this.album = theAlbum;
        this.firePropertyChange("album", oldAlbum, this.album);
    }

    /**
     * Title of this recording.
     * @return title of this recording
     */
    public String getTitle() {
        return title;
    }

    /**
     * Specify the theTitle of this recording. This will be written into
     * the meta-data of the MP3. Property listeners
     * are notified if this value changes.
     *
     * @param theTitle theTitle of this recording
     */
    public void setTitle(final String theTitle) {
        String oldTitle = this.title;
        this.title = theTitle;
        this.firePropertyChange("title", oldTitle, this.title);
    }

    /**
     * Date of this recording.
     * @return date of this recording
     */
    public Date getDate() {
        return date;
    }

    /**
     * Specify the date of this recording. Property listeners
     * are notified if this value changes.
     *
     * @param theDate date of this recording
     */
    public void setDate(final Date theDate) {
        System.out.println("date is now " + theDate);
        Date oldDate = this.date;
        this.date = theDate;
        this.firePropertyChange("date", oldDate, this.date);
    }

    /**
     * Comments associated with this recording.
     * @return comments associated with this recording
     */
    public String getComments() {
        return comments;
    }

    /**
     * Specify the comments associated with this recording. This will be written
     * into the meta-data of the MP3. Property listeners are notified if this
     * value changes.
     *
     * TODO: Bug currently prevents this being written. Script problem?
     *
     * @param theComments comment associated with this recording
     */
    public void setComments(final String theComments) {
        String oldComments = this.comments;
        this.comments = theComments;
        this.firePropertyChange("comments", oldComments, this.comments);
    }

    /**
     * Base of the source address. For example,
     * http://www.stufftolistento.com/dir/fred.htm has a base of fred.htm.
     * @return trimmed down filename of source
     */
    public String getSourceBase() {
        return Util.getBase(this.getSource());
    }

    /**
     * Base name without the extension.For example,
     * http://www.stufftolistento.com/dir/fred.htm has a base name of fred.
     * @return trimmed down fiel name of source without extension
     */
    public String getSourceBaseName() {
        return Util.getBaseName(this.getSource());
    }

    /**
     * The source of the stream. Generally the URL passed to mplayer.
     * @return url of source stream
     */
    public String getSource() {
        return source;
    }

    /**
     * Percentage of buffering complete.
     * @return percent buffer complete
     */
    public int getPerCentProgress() {
        return perCentProgress;
    }

    /**
     * Whether this stream should be imported to iTunes when complete.
     * @return true if should import to iTunes, false otherwise
     */
    public boolean isImportITunes() {
        return importITunes;
    }

    /**
     * Specify whether this stream should be imported to iTunes when complete.
     *
     * @param shouldImport true if should import to iTunes, false otherwise
     */
    public void setImportITunes(final boolean shouldImport) {
        boolean oldImportITunes = this.importITunes;
        this.importITunes = shouldImport;
        this.firePropertyChange("importITunes", new Boolean(oldImportITunes),
                new Boolean(this.importITunes));
    }

    /**
     * Whether this stream should be recorded as stereo.
     * @return true if should be record as stereo, false otherwise
     */
    public boolean isStereo() {
        return stereo;
    }

    /**
     * Specify whether this stream should be recorded as whetherStereo.
     *
     * @param whetherStereo true if should be record as stereo, false otherwise
     */
    public void setStereo(final boolean whetherStereo) {
        boolean oldStereo = this.stereo;
        this.stereo = whetherStereo;
        this.firePropertyChange("stereo", new Boolean(oldStereo),
                new Boolean(this.stereo));
    }

    /**
     * Text description of the current status (eg &quot;buffering 5%&quot;).
     * @return text version of the current status
     */
    public String getStatusDesc() {
        if (this.getStatus() == BUFFERING) {
            return resBundle.getString("_(buffering_") + getPerCentProgress()
            + "%)";
        } else if (this.getStatus() == READING) {
            return resBundle.getString("_(recording)");
        }
        return "";
    }

    public void addListener(StreamListener listener) {
        listeners.add(listener);
    }

    public void removeListener(StreamListener listener) {
        listeners.remove(listener);
    }

    private void clearListeners() {
        listeners.clear();
    }

    public void notifyListeners(String line) {
        for (int i = 0; i < listeners.size(); i++) {
            StreamListener listener = (StreamListener)listeners.get(i);
            listener.receiveLine(line);
        }
    }

    /**
     * String description of this stream.
     * @return description of this stream
     */
    public String toString() {
        return this.getSource() + " " + getStatusDesc();
    }
}
